/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package fr.loof.fonzie;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import javax.persistence.EntityManager;
import javax.persistence.LockModeType;
import javax.persistence.Query;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.Assert;

import fr.loof.fonzie.keywords.Aggregate;

/**
 * AspectJ Aspect to intercept calls to Fonzie 'dynamic' methods and execute a general purpose component as replacement
 * to hand-made code.
 * 
 * @author <a href="nicolas@apache.org">Nicolas De Loof</a>
 */
@Aspect
public class ORMAspect extends AbstractORMAspect
{
    protected static Logger logger = LoggerFactory.getLogger( ORMAspect.class );

    /**
     * When Entities belong to a class hierarchy, developpers cannot safely use the instanceof operation as the
     * implementation in most cases uses Proxies (spec doesn't say anything about this). The result of a JPA query (or
     * navigation in persistent model) on the parent class may return a proxy on this class, that extends the parent
     * class but not the child class, event if the database entry maps to the child one. Base on this technical issue,
     * such code may fail :
     * 
     * <pre>
     *    Vehicle v = getBatMobile(); // Retrieved from JPA
     *    assertTrue( v instanceOf Car );
     * </pre>
     * 
     * Fonzie instanceOf method will detect a proxy and retrieve if necessary the underlying object to check the type
     * assignment.
     */
    @Around( "call( boolean (@javax.persistence.Entity *).instanceOf( Class ) )" )
    public boolean instanceOf( ProceedingJoinPoint jp )
    {
        Object entity = jp.getTarget();
        if ( entity == null )
        {
            return false;
        }
        Class<?> type = (Class<?>) jp.getArgs()[0];

        return JPAUtils.isAssignableFrom( type, entity );
    }

    /**
     * Proxy-safe cast to another @Entity type. As JPA implementation use proxies, developpers cannot safelly cast an
     * entity to a child class.
     * <p>
     * A current workaround is to "unproxy" the entity, and then access the underlying object, that can be casted. A
     * side effect is that this one is not managed by the EntityManager. Updating it's datas or navigating it's
     * relations will not result in the expected JPA support.
     * <p>
     * When the target object is not assignable to the expected type, Fonzie will retrieve it's @Id and load the entity
     * with it, so that a managed Entity is returned.
     * 
     * <pre>
     * public abstract class Vehicle
     * {
     *     public native &lt;T extends Vehicle&gt; T as( Class&lt;T&gt; type );
     * }
     * 
     * // unsafe cast : may cause a ClassCastException with JPA Proxies
     * Car c = (Car) vehicle;
     * 
     * // proxy-safe Fonzie cast
     * Car c = vehicle.as( Car.class );
     * </pre>
     */
    @Around( "call( * (@javax.persistence.Entity *).as( Class ) )" )
    public Object as( ProceedingJoinPoint jp )
    {
        Object entity = jp.getTarget();
        if ( entity == null )
        {
            return null;
        }
        Class<?> type = (Class<?>) jp.getArgs()[0];
        if ( type.isInstance( entity ) )
        {
            return entity;
        }
        Object id = JPAUtils.getId( entity );
        logger.debug( "reload entity {} as {} using ID {}", new Object[] { entity, type, id } );
        Assert.notNull( id, "entity ID is not set, reload as " + type + " is not possible." );
        return entityManager.find( type, id );
    }

    @Around( "call( * (@javax.persistence.Entity *).persist() )" )
    public void persist( ProceedingJoinPoint jp )
    {
        Object entity = jp.getTarget();
        entityManager.persist( entity );
    }

    @Around( "call( * (@javax.persistence.Entity *).merge() )" )
    public Object merge( ProceedingJoinPoint jp )
    {
        Object entity = jp.getTarget();
        return entityManager.merge( entity );
    }

    @Around( "call( * (@javax.persistence.Entity *).remove() )" )
    public void remove( ProceedingJoinPoint jp )
    {
        Object entity = jp.getTarget();
        entityManager.remove( entity );
    }

    @Around( "call( static * (@javax.persistence.Entity *).find(..) )" )
    @SuppressWarnings( "unchecked" )
    public Object findById( ProceedingJoinPoint jp )
    {
        Class entityClass = jp.getSignature().getDeclaringType();
        Object primaryKey = jp.getArgs()[0];
        return entityManager.find( entityClass, primaryKey );
    }

    @Around( "call( static * (@javax.persistence.Entity *).getReference(..) )" )
    @SuppressWarnings( "unchecked" )
    public Object getReference( ProceedingJoinPoint jp )
    {
        Class entityClass = jp.getSignature().getDeclaringType();
        Object primaryKey = jp.getArgs()[0];
        return entityManager.getReference( entityClass, primaryKey );
    }

    @Around( "call( * (@javax.persistence.Entity *).lock( javax.persistence.LockModeType ) )" )
    public void lock( ProceedingJoinPoint jp )
    {
        Object entity = jp.getTarget();
        LockModeType lockMode = (LockModeType) jp.getArgs()[0];
        entityManager.lock( entity, lockMode );
    }

    @Around( "call( * (@javax.persistence.Entity *).refresh() )" )
    public void refresh( ProceedingJoinPoint jp )
    {
        Object entity = jp.getTarget();
        entityManager.refresh( entity );
    }

    @Around( "call( boolean (@javax.persistence.Entity *).isManaged() )" )
    public boolean contains( ProceedingJoinPoint jp )
    {
        Object entity = jp.getTarget();
        return entityManager.contains( entity );
    }

    @Around( "call( static * (@javax.persistence.Entity *).findWithCriterias(..) )" )
    @SuppressWarnings( "unchecked" )
    public Object findWithCriterias( ProceedingJoinPoint jp )
    {
        MethodSignature signature = (MethodSignature) jp.getSignature();
        final Object[] args = jp.getArgs();

        final Class entityClass = signature.getDeclaringType();
        final Criteria[] criterias = (Criteria[]) args[0];

        PersistenceQuery query = new PersistenceQuery()
        {
            public Class getEntity()
            {
                return entityClass;
            }

            public Query createQuery( EntityManager entityManager )
            {
                StringBuilder stb = new StringBuilder( "select e from " + entityClass.getSimpleName() + " e" );
                String separator = " where ";
                for ( Criteria criteria : criterias )
                {
                    if ( criteria.isEmpty() )
                    {
                        continue;
                    }
                    stb.append( separator );
                    separator = " and ";
                    stb.append( criteria.toString() );
                }
                return entityManager.createQuery( stb.toString() );
            }

            public Iterator<String> parameters()
            {
                List<String> names = new LinkedList<String>();
                for ( Criteria criteria : criterias )
                {
                    names.addAll( criteria.getParams().keySet() );
                }
                return names.iterator();
            }
        };

        List<Object> values = new ArrayList<Object>();
        for ( Criteria criteria : criterias )
        {
            values.addAll( criteria.getParams().values() );
        }
        logger.debug( "Execute parsed 'findByCriteria' query {}", query );
        return queryExecutor.execute( query, values.toArray(), isSingleResult( signature ) );
    }

    @Around( "call( static * (@javax.persistence.Entity *).findWhere*(..) )" )
    public Object findWhere( ProceedingJoinPoint jp )
    {
        return proceedFind( jp, "findWhere" );
    }

    @Around( "call( static * (@javax.persistence.Entity *).findBy*(..) )" )
    public Object findBy( ProceedingJoinPoint jp )
    {
        return proceedFind( jp, "findBy" );
    }

    @Around( "call( static * (@javax.persistence.Entity *).findAll(..) )" )
    @SuppressWarnings( "unchecked" )
    public Object findAll( ProceedingJoinPoint jp )
    {
        MethodSignature signature = (MethodSignature) jp.getSignature();
        Class[] types = signature.getParameterTypes();
        Object[] args = jp.getArgs();

        Class entityClass = signature.getDeclaringType();
        PersistenceQuery query = queryParser.parse( entityClass, Aggregate.ALL, "", types );
        logger.debug( "Execute parsed 'findAll' query {}", query );
        return queryExecutor.execute( query, args, isSingleResult( signature ) );
    }

    @Around( "call( static long (@javax.persistence.Entity *).countBy*(..) )" )
    @SuppressWarnings( "unchecked" )
    public Object countBy( ProceedingJoinPoint jp )
    {
        MethodSignature signature = (MethodSignature) jp.getSignature();
        Class[] types = signature.getParameterTypes();
        Object[] args = jp.getArgs();

        Class entityClass = signature.getDeclaringType();
        String name = signature.getName();
        String criteria = name.substring( "countBy".length() );
        PersistenceQuery query = queryParser.parse( entityClass, Aggregate.COUNT, criteria, types );
        logger.debug( "Execute parsed 'countBy' query {}", query );
        return queryExecutor.execute( query, args, isSingleResult( signature ) );
    }

    @Around( "call( static long (@javax.persistence.Entity *).count() )" )
    @SuppressWarnings( "unchecked" )
    public Object count( ProceedingJoinPoint jp )
    {
        MethodSignature signature = (MethodSignature) jp.getSignature();
        Class[] types = signature.getParameterTypes();
        Object[] args = jp.getArgs();

        Class entityClass = signature.getDeclaringType();
        PersistenceQuery query = queryParser.parse( entityClass, Aggregate.COUNT, "", types );
        logger.debug( "Execute parsed 'count' query {}", query );
        return queryExecutor.execute( query, args, isSingleResult( signature ) );
    }

    @SuppressWarnings( "unchecked" )
    protected final Object proceedFind( ProceedingJoinPoint jp, String prefix )
    {
        MethodSignature signature = (MethodSignature) jp.getSignature();
        Class[] types = signature.getParameterTypes();
        Object[] args = jp.getArgs();

        Class entityClass = signature.getDeclaringType();
        String name = signature.getName();
        String criteria = name.substring( prefix.length() );
        PersistenceQuery query = queryParser.parse( entityClass, Aggregate.ALL, criteria, types );
        logger.debug( "Execute parsed 'findBy' query {}", query );
        return queryExecutor.execute( query, args, isSingleResult( signature ) );
    }

    protected boolean isSingleResult( MethodSignature method )
    {
        return !Collection.class.isAssignableFrom( method.getReturnType() );
    }

}
